package clue.engine2;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Random;
import java.util.Set;

import org.javatuples.Triplet;

import clue.board.Board;
import clue.board.Path;
import clue.board.Square;
import clue.board.Square.SquareType;
import clue.cards.Card;
import clue.cards.RoomCard;
import clue.cards.SuspectCard;
import clue.cards.WeaponCard;
import clue.enums.Room;
import clue.enums.Suspect;
import clue.enums.Weapon;
import clue.players.Player;
import clue.players2.Player2;

/**
 *
 * @author Shawn
 */
public class GameEngine2 {
	private final Deck deck;
	private final Random random;
	private final SuspectCard suspect;
	private final RoomCard room;
	private final WeaponCard weapon;
	private final Player2[] players;
	private HashMap<Player2, Square> playerSquare;
	private HashMap<Player2, Room> playerLocation;
	private HashMap<Player2, Integer> playerToID;
	private HashMap<Integer, Player2> IDToPlayer;
	private HashMap<Player2, Set<Card>> playerHands;
	private boolean isGameOver;
	private int playersStillInGame;
	private HashSet<Player2> playersOutOfGame;
	private int winningPlayerID = -1;
	private Board board;
	
	public GameEngine2(Player2[] players) {
		this.players = players;
		deck = new Deck();
		playerToID = new HashMap<>();
		IDToPlayer = new HashMap<>();
		playerHands = new HashMap<>();
		playerSquare = new HashMap<>();
		playerLocation = new HashMap<>();
		playersStillInGame = players.length;
		playersOutOfGame = new HashSet<>();
		// first we must choose each type of card
		suspect = deck.removeSuspectCard();
		room = deck.removeRoomCard();
		weapon = deck.removeWeaponCard();
		board = new Board();
		// now mix together the rest
		deck.mixCards();
		
		// In addition, we give each player a unique ID
		HashSet<Integer> alreadyTakenID = new HashSet<>();
		// TODO: consider using true randomness
		random = new Random();
		int i = 0;
		for (Player2 player : players) {
			// now place each player at a corresponding location
			playerSquare.put(player, board.startingLocs[i++]);
			playerLocation.put(player, null);
			int randomInt;
			do {
				randomInt = random.nextInt(100000);
			} 
			while (alreadyTakenID.contains(randomInt));
			alreadyTakenID.add(randomInt);
			playerToID.put(player, randomInt);
			IDToPlayer.put(randomInt, player);
			playerHands.put(player, new HashSet<Card>());
		}
		
		// now deal the remaining cards
		int index = 0;
		while (!deck.isMixedPileEmpty()) {
			playerHands.get(players[index]).add(deck.removeMixedCard());
			index = (index + 1) % players.length;
		}
		
		isGameOver = false;
	}
	
	public int getWinnerID() {
		return winningPlayerID;
	}
	
	public void play() {
		GameState2 gs = new GameState2(
				players,
				board,
				playerLocation,
				playerSquare,
				playerHands,
				playerToID);
		
		for (Player2 p : players) {
			p.gameStarted(gs);
		}
		
		while (!isGameOver) {
			for (int i = 0; i < players.length; i++) {
				Player2 p = players[i];
				Integer pID = playerToID.get(p);
				if (playersOutOfGame.contains(p))
					continue;
				
				int dice = random.nextInt(6) + 1;
				
				Room room = null;
				Path path = p.move(gs, dice);
				
				if (path.cost > dice)
					throw new RuntimeException("Player " +
											   pID +
											   " returned path longer than dice"
											   );
				
				// verify path is connected
				Square current = playerSquare.get(p);
				for (Square s : path.path) {
					if (!current.neighbors.contains(s))
						throw new RuntimeException("Player " +
								   pID +
								   " returned invalid path"
								   );
					current = s;
					if (current.type == SquareType.ROOM) {
						room = current.room;
						break;
					}
				}
				
				if (room != null && room.equals(playerLocation.get(p)))
					throw new RuntimeException("Player " +
											   pID +
											   " moved into the same room " +
											   room);
				gs.IDToSquare.put(pID, current);
				playerSquare.put(p, current);
				gs.IDToLocation.put(pID, room);
				playerLocation.put(p, room);
				
				// inform other players of this move
				for (int j = ((i + 1) % players.length);
						 j != i;
						 j = ((j + 1) % players.length)) {
					players[j].informMove(gs, pID, current);
				}
				
				// now make a suggestion if in a room
				if (room != null) {
					Triplet<Room, Suspect, Weapon> suggestion = p.suggest(gs);
					
					// first we have to check to see if this is a valid suggestion
					if (suggestion == null)
						throw new RuntimeException("Invalid suggestion: Player " +
												   pID +
												   " returned null suggestion");
					
					if (!suggestion.getValue0().equals(room))
						throw new RuntimeException("Invalid suggestion: Player " +
												   pID +
												   " is not in room " +
												   room);
					
					// inform other players of this suggestion
					for (int j = ((i + 1) % players.length);
					     j != i;
						 j = ((j + 1) % players.length)) {
						players[j].informSuggestion(gs, pID, suggestion);
					}
					
					// now we go around until someone refutes
					for (int j = ((i + 1) % players.length);
						 j != i;
						 j = ((j + 1) % players.length)) {
						Player2 p2 = players[j];
						Integer p2ID = playerToID.get(p2);
						Card c = p2.refute(gs, pID, suggestion);
						
						if (c == null) {
							// check that p2 is not cheating by not refuting
							// when he actually could
							for (Card p2card : playerHands.get(p2)) {
								if (p2card instanceof RoomCard &&
										(((RoomCard) p2card).getRoom()
												.equals(suggestion.getValue0()))) {
										throw new RuntimeException(
												"Player " +
												p2ID +
												" cheated: could refute" +
												" suggestion with " +
												suggestion.getValue0());
							    } else if (p2card instanceof WeaponCard &&
							    		(((WeaponCard) p2card).getWeapon()
							    				.equals(suggestion.getValue2()))) {
							    		throw new RuntimeException(
							    				"Player " +
							    				p2ID +
							    				" cheated: could refute" +
							    				" suggestion with " +
							    				suggestion.getValue2());
							    } else if (p2card instanceof SuspectCard &&
							    		(((SuspectCard) p2card).getSuspect()
							    				.equals(suggestion.getValue1()))) {
							    		throw new RuntimeException(
							    				"Player " +
							    				p2ID + 
							    				" cheated: could refute" +
							    				" suggestion with " +
							    				suggestion.getValue1());
							    }
							}
							
							// inform others of p2's failure to refute
							for (Player2 other : players) {
								if (other == p2)
									continue;
								other.informFailureToRefute(gs, p2ID, suggestion);
							}
						} else {
							// check that this card is in p2's hand
							if (!playerHands.get(p2).contains(c))
								throw new RuntimeException("Invalid refutation: Player " + p2ID +
														   " does not have card " + c);
							
							// check that this card actually refutes
							if ((c instanceof RoomCard && !((RoomCard) c).getRoom().equals(suggestion.getValue0())) ||
								(c instanceof WeaponCard && !((WeaponCard) c).getWeapon().equals(suggestion.getValue2())) ||
								(c instanceof SuspectCard && !((SuspectCard) c).getSuspect().equals(suggestion.getValue1()))) {
									throw new RuntimeException("Invalid refutation: " + p2ID + " does not actually refute");
						    }
							
							// inform suggesting player of refutation
							p.informRefutation(gs, p2ID, c);
							
							// inform others of refutation without revealing refuting card
							for (Player2 other : players) {
								if (other == p2 || other == p)
									continue;
								other.informRefutationHidden(gs, p2ID, pID, suggestion);
							}
							break;
						}
					}
				}
				// make accusation
				Triplet<Room, Suspect, Weapon> accusation = p.accuse(gs);
				if (accusation != null) {
					// check to see if it's correct
					if (this.suspect.getSuspect().equals(accusation.getValue1()) && 
					    this.room.getRoom().equals(accusation.getValue0()) &&
					    this.weapon.getWeapon().equals(accusation.getValue2())) {
						
						// inform everyone that game is over and who won
						for (Player2 p2 : players) {
							p2.gameEnded(gs, pID, accusation);
						}
						
						//System.out.println("Player " + pID + " correctly accused " + this.room + "," + this.suspect + "," + this.weapon + ".");
						//System.out.println("GAME OVER!");
						isGameOver = true;
						winningPlayerID = pID;
						break;
					} else {
						// take p out of the game. He can no longer move and suggest. However, he is still there to refute suggestions.
						playersOutOfGame.add(p);
						playersStillInGame--;
						
						for (Player2 p2 : players) {
							p2.informFailedAccusation(gs, pID, accusation);
						}
						
						if (playersStillInGame <= 1) {
							// if there is only one player left, he wins by default
							// find him first
							
							Player2 winner = null;
							for (Player2 temp : players) {
								if (playersOutOfGame.contains(temp))
									continue;
								winner = temp;
								break;
							}
							
							for (Player2 p3 : players) {
								p3.gameEnded(gs, playerToID.get(winner), null);
							}
							//System.out.println("Player " + playerToID.get(winner) + " wins by elimination.");
							isGameOver = true;
							winningPlayerID = playerToID.get(winner);
							break;
						}
					}
				}
			}
		}
	}
}
